winbrew_app\operations\remove/
execution.rs

1//! Engine-specific package removal and filesystem cleanup.
2//!
3//! The execution phase consumes a precomputed [`RemovalPlan`] and mutates the
4//! database and filesystem state accordingly. The exact removal strategy
5//! depends on the package engine:
6//!
7//! - MSIX, MSI, and native executable packages are removed through the engine
8//!   first and then cleaned from disk.
9//! - Zip and portable packages are staged into a trash directory before the
10//!   database row is deleted so the install tree can be restored if metadata
11//!   removal fails.
12//!
13//! The functions here favor best-effort cleanup. Filesystem failures after the
14//! removal path has already made progress are logged when practical so the main
15//! removal outcome stays focused on whether the package was successfully removed.
16
17use anyhow::Context;
18use tracing::{debug, warn};
19
20use std::path::{Path, PathBuf};
21
22use crate::core::fs::cleanup_path;
23use crate::core::paths::{install_root_from_package_dir, package_journal_file_at};
24use crate::database;
25use crate::database::package_journal_key;
26use crate::engines::{EngineKind, PackageEngine};
27use crate::operations::shims;
28
29use super::{RemovalError, RemovalPlan, Result};
30use crate::models::domains::installed::InstalledPackage;
31
32/// Execute package removal using a fresh database connection.
33///
34/// This is the public execution entry point. It only acquires the database
35/// connection and then delegates the actual work to the shared removal engine.
36pub fn execute_removal(plan: &RemovalPlan, force: bool) -> Result<()> {
37    let conn = database::get_conn()?;
38    let shims_root =
39        install_root_from_package_dir(Path::new(&plan.package.install_dir)).join("shims");
40
41    execute_removal_with_conn(plan, force, &shims_root, &conn)
42}
43
44/// Execute a removal plan with a caller-provided database connection.
45///
46/// This function enforces the removal policy, selects the correct engine kind,
47/// and applies the engine-specific cleanup path. When `force` is false, the
48/// presence of dependent packages blocks removal before any mutation happens.
49fn execute_removal_with_conn(
50    plan: &RemovalPlan,
51    force: bool,
52    shims_root: &std::path::Path,
53    conn: &database::DbConnection,
54) -> Result<()> {
55    debug!(
56        package = plan.package.name.as_str(),
57        force, "starting remove"
58    );
59
60    if !force && !plan.dependents.is_empty() {
61        // Remove a package directory, database row, and any leftover staging artifacts.
62        // The helper is intentionally engine-agnostic. It handles the shared cleanup
63        // patterns used by both removal strategies and leaves the engine-specific work
64        // to the caller.
65        return Err(RemovalError::DependentPackagesBlocked {
66            name: plan.package.name.clone(),
67            dependents: plan.dependents.join(", "),
68        });
69    }
70
71    let install_dir = PathBuf::from(&plan.package.install_dir);
72    let engine_kind = plan.package.engine_kind;
73    let commands = match database::list_commands_for_package(conn, &plan.package.name) {
74        Ok(commands) => commands,
75        Err(err) => {
76            warn!(
77                package = plan.package.name.as_str(),
78                error = %err,
79                "failed to read package commands for shim cleanup"
80            );
81            Vec::new()
82        }
83    };
84
85    match engine_kind {
86        EngineKind::Msix | EngineKind::Msi | EngineKind::NativeExe | EngineKind::Font => {
87            engine_kind.remove(&plan.package)?;
88
89            if install_dir.exists()
90                && let Err(err) = cleanup_path(&install_dir)
91            {
92                warn!(
93                    "failed to remove package directory for {}: {err}",
94                    plan.package.name
95                );
96            }
97
98            database::delete_package(conn, &plan.package.name)?;
99            if !commands.is_empty()
100                && let Err(err) = shims::remove_shim_files(shims_root, &commands)
101            {
102                warn!(
103                    package = plan.package.name.as_str(),
104                    error = %err,
105                    "failed to remove package shims"
106                );
107            }
108
109            if let Err(err) =
110                cleanup_committed_journal(&install_dir, &plan.package.name, &plan.package.version)
111            {
112                warn!(
113                    package = plan.package.name.as_str(),
114                    error = %err,
115                    "failed to remove committed package journal"
116                );
117            }
118        }
119        EngineKind::Zip | EngineKind::Portable => {
120            if install_dir.exists() {
121                let trash_dir = install_dir.with_extension("trash");
122
123                cleanup_path(&trash_dir).context("failed to clean up old trash directory")?;
124
125                std::fs::rename(&install_dir, &trash_dir)
126                    .context("failed to stage package for removal")?;
127
128                let trash_package = InstalledPackage {
129                    install_dir: trash_dir.to_string_lossy().into_owned(),
130                    ..plan.package.clone()
131                };
132
133                if let Err(err) = database::delete_package(conn, &plan.package.name) {
134                    let _ = std::fs::rename(&trash_dir, &install_dir);
135                    return Err(RemovalError::Unexpected(err));
136                }
137
138                if !commands.is_empty()
139                    && let Err(err) = shims::remove_shim_files(shims_root, &commands)
140                {
141                    warn!(
142                        package = plan.package.name.as_str(),
143                        error = %err,
144                        "failed to remove package shims"
145                    );
146                }
147
148                if let Err(err) = cleanup_committed_journal(
149                    &install_dir,
150                    &plan.package.name,
151                    &plan.package.version,
152                ) {
153                    warn!(
154                        package = plan.package.name.as_str(),
155                        error = %err,
156                        "failed to remove committed package journal"
157                    );
158                }
159
160                if let Err(err) = engine_kind.remove(&trash_package) {
161                    warn!(
162                        "failed to completely remove trash for {}: {err}",
163                        plan.package.name
164                    );
165                }
166            } else {
167                database::delete_package(conn, &plan.package.name)?;
168                if !commands.is_empty()
169                    && let Err(err) = shims::remove_shim_files(shims_root, &commands)
170                {
171                    warn!(
172                        package = plan.package.name.as_str(),
173                        error = %err,
174                        "failed to remove package shims"
175                    );
176                }
177
178                if let Err(err) = cleanup_committed_journal(
179                    &install_dir,
180                    &plan.package.name,
181                    &plan.package.version,
182                ) {
183                    warn!(
184                        package = plan.package.name.as_str(),
185                        error = %err,
186                        "failed to remove committed package journal"
187                    );
188                }
189            }
190        }
191    }
192
193    debug!(
194        package = plan.package.name.as_str(),
195        force, "remove completed"
196    );
197
198    Ok(())
199}
200
201fn cleanup_committed_journal(
202    install_dir: &Path,
203    package_name: &str,
204    package_version: &str,
205) -> anyhow::Result<()> {
206    let install_root = install_root_from_package_dir(install_dir);
207    let package_key = package_journal_key(package_name, package_version);
208    let journal_path = package_journal_file_at(&install_root, &package_key);
209
210    cleanup_path(&journal_path).with_context(|| {
211        format!(
212            "failed to remove committed journal at {}",
213            journal_path.display()
214        )
215    })?;
216
217    Ok(())
218}